﻿using System.Windows;
using System.Windows.Shapes;
using System.Windows.Media;
using System.Windows.Controls;
using System;

namespace MidiPianoTray
{
    public sealed partial class MainWindow : Window, IDisposable
    {
        private System.Windows.Forms.NotifyIcon notifyIcon = null;

        private const int mNumKeys = 88;
        private SolidColorBrush mBlackBrush = new SolidColorBrush(Colors.Black);
        private SolidColorBrush mWhiteBrush = new SolidColorBrush(Colors.White);
        private SolidColorBrush mRedBrush   = new SolidColorBrush(Colors.Red);

        private Rectangle [] mKeyRectangles = new Rectangle[mNumKeys];
        private int []       mKeyVelocityTable = new int[mNumKeys];

        private Midi.HMidiIn  mHMidiIn = new Midi.HMidiIn();
        private Midi.HMidiOut mHMidiOut = new Midi.HMidiOut();
        enum MidiInStatus {
            Closed,
            Opened,
            Started,
        };

        private MidiInStatus mMidiInStatus = MidiInStatus.Closed;

        enum MidiOutStatus {
            Closed,
            Opened,
        };

        private MidiOutStatus mMidiOutStatus = MidiOutStatus.Closed;

        private Midi.MidiInProc  mMidiInProc;
        private Midi.MidiOutProc mMidiOutProc;

        private bool mMaxVelo = true;
        private bool mWindowLoaded = false;

        private void Dispose(bool disposing) {
            if (disposing) {
                // dispose managed resources
                notifyIcon.Dispose();
            }
            // free native resources
        }

        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        
        private bool[] mBlackKeyTableFromA = new bool [] {
                //A    A#     B      C      C#     D     D#    E      F      F#    G      G#
                false, true, false, false, true, false, true, false, false, true, false, true,
            };

        private void PrepareKeyboard() {
            canvasKeyboard.Children.Clear();

            var keyPositionFromA = new int [] {
                0,1,2,4,5,6,7,8,10,11,12,13
            };

            var blackKeyOffset = new int[] {
                //A    A#     B      C      C#     D     D#    E      F      F#    G      G#
                0,     1,     0,     0,     -1,    0,    1,    0,     0,     -1,   0,     0
            };

            // これだと鍵盤半分ぶん少ない値になる
            //double keyWidth = ((mNumKeys/12)*7+keyPositionFromA[mNumKeys%12]*0.5);

            double keyWidth = (((mNumKeys+1)/12)*7+keyPositionFromA[(mNumKeys+1)%12]*0.5);

            for (int i=0; i < mNumKeys; ++i) {
                int note = i % 12;
                int octave = i / 12;

                var r = new Rectangle();

                double x = canvasKeyboard.ActualWidth * (octave * 7 + keyPositionFromA[note]*0.5) / keyWidth;

                r.Stroke = mBlackBrush;
                if (mBlackKeyTableFromA[note]) {
                    // black key
                    r.Height = canvasKeyboard.ActualHeight * 2 / 3;
                    r.Width  = canvasKeyboard.ActualWidth / keyWidth * 0.8;
                    x += canvasKeyboard.ActualWidth / keyWidth * 0.1;
                    x += canvasKeyboard.ActualWidth / keyWidth * 0.1 * blackKeyOffset[note];
                    r.Fill = mBlackBrush;
                    Canvas.SetZIndex(r, 1);
                } else {
                    // white key
                    r.Height = canvasKeyboard.ActualHeight;
                    r.Width  = canvasKeyboard.ActualWidth / keyWidth;
                    r.Fill = mWhiteBrush;
                    Canvas.SetZIndex(r, 0);
                }

                Canvas.SetLeft(r, x);
                Canvas.SetTop(r, 0);
                canvasKeyboard.Children.Add(r);
                mKeyRectangles[i] = r;
            }
        }

        private void UpdateKeyboard() {
            for (int i=0; i < mNumKeys; ++i) {
                int note = i % 12;

                var r = mKeyRectangles[i];

                if (0 < mKeyVelocityTable[i]) {
                    r.Fill = mRedBrush;
                    continue;
                }

                if (mBlackKeyTableFromA[note]) {
                    r.Fill = mBlackBrush;
                } else {
                    r.Fill = mWhiteBrush;
                }
            }
        }

        private void UpdateOutputDeviceList() {
            listBoxMidiOutputDevices.Items.Clear();

            int nOut = Midi.OutGetNumDevs();
            for (uint i=0; i < nOut; ++i) {
                var caps = Midi.OutGetDevCaps(i);
                if (null == caps) {
                    continue;
                }

                listBoxMidiOutputDevices.Items.Add(caps.name);
                if (0 == i) {
                    listBoxMidiOutputDevices.SelectedIndex = 0;
                }

                var prevDeviceName = Properties.Settings.Default.MidiOutputDeviceName;
                if (null != prevDeviceName && 0 < prevDeviceName.Length) {
                    if (0 == string.Compare(prevDeviceName, caps.name, StringComparison.Ordinal)) {
                        listBoxMidiOutputDevices.SelectedIndex = (int)i;
                    }
                }
            }
        }

        private void UpdateInputDeviceList() {
            listBoxMidiInputDevices.Items.Clear();

            int nOut = Midi.InGetNumDevs();
            for (uint i=0; i < nOut; ++i) {
                var caps = Midi.InGetDevCaps(i);
                if (null == caps) {
                    continue;
                }

                listBoxMidiInputDevices.Items.Add(caps.name);
                if (0 == i) {
                    listBoxMidiInputDevices.SelectedIndex = 0;
                }

                var prevDeviceName = Properties.Settings.Default.MidiInputDeviceName;
                if (null != prevDeviceName && 0 < prevDeviceName.Length) {
                    if (0 == string.Compare(prevDeviceName, caps.name, StringComparison.Ordinal)) {
                        listBoxMidiInputDevices.SelectedIndex = (int)i;
                    }
                }
            }
        }

        private void MyMidiInProc(Midi.HMidiIn handle, UInt32 wMsg, UIntPtr dwInstance, UIntPtr dwParam1, UIntPtr dwParam2) {
            uint param = dwParam1.ToUInt32();
            uint velo = (param >> 16) & 0xff;
            uint pitchOrig = (param >> 8) & 0xff;
            int pitch = (int)pitchOrig - 12;

            int octave = (int)(pitchOrig / 12) - 1;
            int note = (int)(pitchOrig % 12);

            uint command = param & 0xff;

            //Console.WriteLine("MidiInProc msg={0:X8} dwParam1={1:X8} octave={2} noteNumber={3} velo={4}",
            //    wMsg, param, octave, pitchOrig, velo);

            if (Convert.ToUInt32(Midi.Command.NoteOff) != command
                && Convert.ToUInt32(Midi.Command.NoteOn) != command) {
                return;
            }

            if (Convert.ToUInt32(Midi.Command.NoteOff) == command) {
                velo = 0;
            }

            // 88key piano starts from A==9
            if (9 <= pitch && (pitch-9) < mNumKeys) {
                mKeyVelocityTable[pitch - 9] = (int)velo;
            }

            if (mMidiOutStatus == MidiOutStatus.Opened) {
                uint outVelo = velo;
                if (0 < velo && mMaxVelo) {
                    // max volume == 127
                    outVelo = 127;
                }
                uint outCommand = Convert.ToUInt32(Midi.Command.NoteOn);
                if (velo == 0) {
                    outCommand = Convert.ToUInt32(Midi.Command.NoteOff);
                }
                Midi.OutShortMsg(mHMidiOut, (outVelo << 16) + (pitchOrig<<8) + outCommand);
            }

            // call UpdateKeyboard() on UI thread
            canvasKeyboard.Dispatcher.BeginInvoke(new Action(this.UpdateKeyboard));
        }

        private bool InitInputDevice() {
            if (listBoxMidiInputDevices.SelectedIndex < 0) {
                return false;
            }

            mMidiInProc = new Midi.MidiInProc(MyMidiInProc);
            uint rv = Midi.InOpen(out mHMidiIn, (uint)listBoxMidiInputDevices.SelectedIndex, mMidiInProc);
            if (0 != rv) {
                return false;
            }

            mMidiInStatus = MidiInStatus.Opened;

            Midi.InReset(mHMidiIn);
            rv = Midi.InStart(mHMidiIn);
            if (0 != rv) {
                return false;
            }

            mMidiInStatus = MidiInStatus.Started;

            return true;
        }

        private void TermInputDevice() {
            if (MidiInStatus.Started == mMidiInStatus) {
                Midi.InStop(mHMidiIn);
                mMidiInStatus = MidiInStatus.Opened;
            }

            if (MidiInStatus.Opened == mMidiInStatus) {
                Midi.InReset(mHMidiIn);
                Midi.InClose(mHMidiIn);
                mHMidiIn = new Midi.HMidiIn();
                mMidiInStatus = MidiInStatus.Closed;
            }

            mMidiInProc = null;
        }

        private void MyMidiOutProc(Midi.HMidiOut handle, UInt32 wMsg, UIntPtr dwInstance, UIntPtr dwParam1, UIntPtr dwParam2) {
            //Console.WriteLine("MidiOutProc");
        }

        private void UpdateInstrument() {
            if (comboBoxInstrument.SelectedIndex < 0) {
                return;
            }

            uint instrument = (uint)comboBoxInstrument.SelectedIndex;
            uint cmd = Convert.ToUInt32(Midi.Command.ProgramChange);
            Midi.OutShortMsg(mHMidiOut, (instrument << 8) + cmd);
        }

        private bool InitOutputDevice() {
            if (listBoxMidiOutputDevices.SelectedIndex < 0) {
                return false;
            }

            mMidiOutProc = new Midi.MidiOutProc(MyMidiOutProc);
            uint rv = Midi.OutOpen(out mHMidiOut, (uint)listBoxMidiOutputDevices.SelectedIndex, mMidiOutProc);
            if (0 != rv) {
                return false;
            }

            UpdateInstrument();

            mMidiOutStatus = MidiOutStatus.Opened;
            return true;
        }

        private void TermOutputDevice() {
            if (MidiOutStatus.Opened == mMidiOutStatus) {
                Midi.OutReset(mHMidiOut);
                Midi.OutClose(mHMidiOut);
                mHMidiOut = new Midi.HMidiOut();
                mMidiOutProc = null;
                mMidiOutStatus = MidiOutStatus.Closed;
            }
        }

        public MainWindow() {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e) {
            notifyIcon = new System.Windows.Forms.NotifyIcon();
            notifyIcon.Click += new EventHandler(notifyIcon_Click);
            notifyIcon.DoubleClick += new EventHandler(notifyIcon_DoubleClick);
            notifyIcon.Icon = Properties.Resources.TypeE_Multi;
            notifyIcon.Visible = true;

            mBlackBrush.Freeze();
            mWhiteBrush.Freeze();
            mRedBrush.Freeze();

            PrepareKeyboard();

            UpdateInputDeviceList();
            UpdateOutputDeviceList();

            comboBoxInstrument.Items.Clear();
            foreach (var name in  Enum.GetNames(typeof(Midi.ProgramType))) {
                comboBoxInstrument.Items.Add(name);
            }
            comboBoxInstrument.SelectedIndex = Properties.Settings.Default.MidiOutputInstrument;

            InitInputDevice();
            InitOutputDevice();

            mMaxVelo = Properties.Settings.Default.FixVelocityToMax;
            checkBoxFixVolume.IsChecked = Properties.Settings.Default.FixVelocityToMax;

            mWindowLoaded = true;
        }


        private void Window_SizeChanged(object sender, SizeChangedEventArgs e) {
            PrepareKeyboard();
        }

        private void buttonRefreshInput_Click(object sender, RoutedEventArgs e) {
            Properties.Settings.Default.MidiInputDeviceName = (string)listBoxMidiInputDevices.SelectedItem;

            TermInputDevice();
            UpdateInputDeviceList();
            InitInputDevice();
        }

        private void buttonRefreshOutput_Click(object sender, RoutedEventArgs e) {
            Properties.Settings.Default.MidiOutputDeviceName = (string)listBoxMidiOutputDevices.SelectedItem;

            TermOutputDevice();
            UpdateOutputDeviceList();
            InitOutputDevice();
        }

        private void buttonQuit_Click(object sender, RoutedEventArgs e) {
            Close();
        }

        private void Window_Closed(object sender, EventArgs e) {
            Term();
        }

        private void Term() {
            TermInputDevice();
            TermOutputDevice();
            Properties.Settings.Default.Save();
        }

        private void checkBoxFixVolume_Click(object sender, RoutedEventArgs e) {
            mMaxVelo = checkBoxFixVolume.IsChecked == true;
            if (mWindowLoaded) {
                Properties.Settings.Default.FixVelocityToMax = mMaxVelo;
            }
        }

        private void notifyIcon_Click(object sender, EventArgs e) {
            this.Visibility = System.Windows.Visibility.Visible;
            this.WindowState = System.Windows.WindowState.Normal;
        }

        private void notifyIcon_DoubleClick(object sender, EventArgs e) {
            this.Visibility = System.Windows.Visibility.Visible;
            this.WindowState = System.Windows.WindowState.Normal;
        }

        private void buttonOK_Click(object sender, RoutedEventArgs e) {
            this.Visibility = System.Windows.Visibility.Hidden;
        }

        private void comboBoxInstrument_SelectionChanged(object sender, SelectionChangedEventArgs e) {
            UpdateInstrument();
            if (0 <= comboBoxInstrument.SelectedIndex) {
                Properties.Settings.Default.MidiOutputInstrument = comboBoxInstrument.SelectedIndex;
            }
        }

        private void Window_StateChanged(object sender, EventArgs e) {
            if (WindowState == System.Windows.WindowState.Minimized) {
                this.Visibility = System.Windows.Visibility.Hidden;
                WindowState = System.Windows.WindowState.Normal;
            }
        }

    }
}
